#include "usb_media_imp.h"
//#include <debug.h>
#include <syncup.h>

#include <libudev.h>

#include <stdio.h>
#include <string.h>

#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>
#include <glib.h>
#include <glib/gstdio.h>


/**
 * @brief Free \c list_head list
 * @param head  Head of list
 * @param type  Type of container structure of \c list_head
 * @param member  Member name of \c list_head in container structure
 */
#define list_free(head, type, member) \
    do { \
        struct list_head *__pos, *__t; \
        list_for_each_safe(__pos, __t, head) { \
            list_del(__pos); \
            g_slice_free(type, list_entry(__pos, type, member)); \
        } \
    } while (0)


/**
 * @name \c usb_media_linux manipulation functions
 */
/*@{*/
static inline void
usb_media_linux_set_dev_node(struct usb_media_linux *um, const char *devnode)
{
    g_strlcpy(um->dev_node, devnode, sizeof(um->dev_node));
}

static inline void
usb_media_linux_set_um_root(struct usb_media_linux *um, const char *um_root)
{
    g_strlcpy(um->um.um_root, um_root, sizeof (um->um.um_root));
}

static inline struct usb_media_linux*
usb_media_linux_new(const char *devnode, const char *mountpoint)
{
    struct usb_media_linux* um = g_slice_new(struct usb_media_linux);
    usb_media_linux_set_um_root(um, mountpoint);
    um->um.syncup_enabled = false;
    usb_media_linux_set_dev_node(um, devnode);
    return um;
}

/**
 * @note \c um->um.um_list is not copied in clone
 */
static inline struct usb_media_linux *
usb_media_linux_clone(struct usb_media_linux *um)
{
    struct usb_media_linux *clone = g_slice_new(struct usb_media_linux);
    strcpy(clone->um.um_root, um->um.um_root);
    clone->um.syncup_enabled = um->um.syncup_enabled;
    strcpy(clone->dev_node, um->dev_node);
    return clone;
}
/*@} End of usb_media_linux manipulation functions */



struct usb_media_monitor {
    struct list_head um_list;
    pthread_rwlock_t um_rwlock;
    struct list_head evt_hdlr_list[UM_EVENT_NUM];
    pthread_mutex_t evt_hdlr_mutex;
    pthread_t tid;
};

#define EXPAND_UM_EVENT(e) LIST_HEAD_INIT(usb_media_mon.evt_hdlr_list[e]),
static struct usb_media_monitor usb_media_mon = {
        .um_list = LIST_HEAD_INIT(usb_media_mon.um_list),
        .um_rwlock = PTHREAD_RWLOCK_INITIALIZER,
        .evt_hdlr_list = { UM_EVENT_LIST },
        .evt_hdlr_mutex = PTHREAD_MUTEX_INITIALIZER
};
#undef EXPAND_UM_EVENT


struct um_event_handler {
    um_event_handler_t func;
    struct list_head evt_hdlr_list;
};


/**
 * @name \c usb_media_monitor manipulation functions
 */
/*@{*/
static inline void
usb_media_monitor_init(struct usb_media_monitor *um_mon)
{
    INIT_LIST_HEAD(&um_mon->um_list);
    pthread_rwlock_init(&um_mon->um_rwlock, NULL);
#define EXPAND_UM_EVENT(e) INIT_LIST_HEAD(&um_mon->evt_hdlr_list[e]);
    UM_EVENT_LIST
#undef EXPAND_UM_EVENT
    pthread_mutex_init(&um_mon->evt_hdlr_mutex, NULL);
}

static inline void
usb_media_monitor_free(struct usb_media_monitor *um_mon)
{
    list_free(&um_mon->um_list, struct usb_media_linux, um.um_list);
    pthread_rwlock_destroy(&um_mon->um_rwlock);
#define EXPAND_UM_EVENT(e) list_free(&um_mon->evt_hdlr_list[e], struct um_event_handler, evt_hdlr_list);
    UM_EVENT_LIST
#undef EXPAND_UM_EVENT
    pthread_mutex_destroy(&um_mon->evt_hdlr_mutex);
}

/**
 * Add \c usb_media_linux into list without checking duplicate
 * @note NOT thread-safe
 */
static inline void
usb_media_monitor_add_um(struct usb_media_monitor *um_mon,
        struct usb_media_linux *um)
{
    list_add_tail(&um->um.um_list, &um_mon->um_list);
}

/**
 * Delete \c usb_media_linux from list; only delete first one found
 * @note NOT thread-safe
 */
static void
usb_media_monitor_del_um(struct usb_media_monitor *um_mon,
        const char *devnode)
{
    struct usb_media_linux *um;
    struct list_head *pos, *t;
    struct list_head *head = &um_mon->um_list;

    list_for_each_safe(pos, t, head) {
        um = list_entry(pos, struct usb_media_linux, um.um_list);
        if (strcmp(um->dev_node, devnode) == 0) {
            list_del(pos);
            g_slice_free(struct usb_media_linux, um);
            break;
        }
    }
}

/**
 * Add um_event_handler into list without checking duplicate
 * @note Thread-safe
 */
static inline void
usb_media_monitor_add_evt_hdlr(struct usb_media_monitor *um_mon,
        enum um_event_type evt_type, um_event_handler_t func) {
    struct um_event_handler *hdlr = g_slice_new(struct um_event_handler);
    hdlr->func = func;
    pthread_mutex_lock(&um_mon->evt_hdlr_mutex);
    list_add_tail(&hdlr->evt_hdlr_list, &um_mon->evt_hdlr_list[evt_type]);
    pthread_mutex_unlock(&um_mon->evt_hdlr_mutex);
}

/**
 * Delete um_event_handler from list; only delete first one found
 * @note Thread-safe
 */
static void
usb_media_monitor_del_evt_hdlr(struct usb_media_monitor *um_mon,
        enum um_event_type evt_type, um_event_handler_t func) {
    struct um_event_handler *hdlr;
    struct list_head *pos, *t;
    struct list_head *head = &um_mon->evt_hdlr_list[evt_type];

    pthread_mutex_lock(&um_mon->evt_hdlr_mutex);
    list_for_each_safe(pos, t, head) {
        hdlr = list_entry(pos, struct um_event_handler, evt_hdlr_list);
        if (hdlr->func == func) {
            list_del(pos);
            break;
        }
    }
    pthread_mutex_unlock(&um_mon->evt_hdlr_mutex);
    g_slice_free(struct um_event_handler, hdlr);
}

/**
 * @note Thread-safe
 */
static inline void
usb_media_monitor_call_evt_hdlr(struct usb_media_monitor *um_mon,
        enum um_event_type evt_type, const struct usb_media *um) {
    struct um_event_handler *hdlr;
    struct list_head *head;

    head = &um_mon->evt_hdlr_list[evt_type];
    pthread_mutex_lock(&um_mon->evt_hdlr_mutex);
    list_for_each_entry(hdlr, head, evt_hdlr_list) {
        hdlr->func(um);
    }
    pthread_mutex_unlock(&um_mon->evt_hdlr_mutex);
}
/*@} End of usb_media_monitor manipulation functions */



/**
 * @brief Return mount point of the device specified by its device node name
 * @return mount point path if exists; if not, return empty string
 * @note NOT a reentrant function
 */
static const char *get_mountpoint(const char *devnode)
{
    static char buf[4096];
    const char *delim = " ";
    char *mountpoint = NULL;
    FILE *fp = fopen("/proc/mounts", "r");

    while (fgets(buf, sizeof (buf), fp)) {
        const char *node = strtok(buf, delim);
        if (strcmp(node, devnode) == 0) {
            mountpoint = strtok(NULL, delim);
            break;
        }
    }

    if (mountpoint == NULL) {
        mountpoint = "";
    } else {
        /* If mount point contain characters like space, it is converted to
         * "\040". This situation should be handled.
         */
        register char *c1, *c2;
        for (c1 = c2 = mountpoint; *c2; ++c1) {
            if (*c2 == '\\') {
                *c1 = (((c2[1] - '0') << 6)
                        | ((c2[2] - '0') << 3)
                        | (c2[3] - '0'));
                c2 += 4;
                continue;
            }
            if (c1 != c2) {
                *c1 = *c2;
            }
            ++c2;
        }
        *c1 = *c2;
    }

    fclose(fp);
    return mountpoint;
}


/**
 * @brief Query all usb media and put them in the list
 *
 * Use this function to get the initial list of available usb media.
 * Usb media list is expected as empty before call of this function.
 * It also triggers UM_ADD event handler when new usb medium added has mount
 * point.
 *
 * @param um_mon  usb_media_monitor context
 * @param udev  udev context
 */
static void
usb_media_monitor_query_um(struct usb_media_monitor *um_mon,
        struct udev *udev)
{
    struct udev_enumerate *enumerate;
    struct udev_list_entry *devices, *dev_list_entry;
    struct udev_device *dev;

    enumerate = udev_enumerate_new(udev);
    udev_enumerate_add_match_subsystem(enumerate, "block");
    udev_enumerate_add_match_property(enumerate, "DEVTYPE", "partition");
    udev_enumerate_scan_devices(enumerate);
    devices = udev_enumerate_get_list_entry(enumerate);
    udev_list_entry_foreach(dev_list_entry, devices) {
        const char *path, *devnode, *mountpoint;
        struct usb_media_linux *um;

        path = udev_list_entry_get_name(dev_list_entry);
        dev = udev_device_new_from_syspath(udev, path);
        if (!udev_device_get_parent_with_subsystem_devtype(dev,
                    "usb", "usb_device")) {
            /* If not a usb device, just ignore it */
            udev_device_unref(dev);
            continue;
        }

        devnode = udev_device_get_devnode(dev);
        mountpoint = get_mountpoint(devnode);
        um = usb_media_linux_new(devnode, mountpoint);
        /* Here it's safe without locking um_list, because during this
         * function executes, no other thread will access um_list.
         */
        usb_media_monitor_add_um(um_mon, um);
        if (mountpoint[0] != '\0') {
            usb_media_monitor_call_evt_hdlr(um_mon, UM_ADD,
                    (struct usb_media *)um);
        }

        udev_device_unref(dev);
    }
    udev_enumerate_unref(enumerate);
}



static void
handle_udev_monitor_event(struct udev_monitor *mon)
{
    struct udev_device *dev;
    const char *action, *devnode;
    struct usb_media_linux *um;

    dev = udev_monitor_receive_device(mon); /* blocking function */
    if (!dev) {
        g_critical("udev_monitor_receive_device() failed");
        return;
    }
    action = udev_device_get_action(dev);
    devnode = udev_device_get_devnode(dev);
    if (strcmp(action, "add") == 0) {
        const char *mountpoint = get_mountpoint(devnode);
        um = usb_media_linux_new(devnode, mountpoint);
        pthread_rwlock_wrlock(&usb_media_mon.um_rwlock);
        usb_media_monitor_add_um(&usb_media_mon, um);
        pthread_rwlock_unlock(&usb_media_mon.um_rwlock);
    } else if (strcmp(action, "remove") == 0) {
        pthread_rwlock_wrlock(&usb_media_mon.um_rwlock);
        usb_media_monitor_del_um(&usb_media_mon, devnode);
        pthread_rwlock_unlock(&usb_media_mon.um_rwlock);
    } else {
        g_warning("unknown udev event: action %s", action);
    }
    udev_device_unref(dev);
}

static void
handle_mount_change_event()
{
    enum um_event_type evt_type;
    struct usb_media_linux *ent;
    struct list_head *head = &usb_media_mon.um_list;

    list_for_each_entry(ent, head, um.um_list) {
        const char *mountpoint = get_mountpoint(ent->dev_node);
        if (strcmp(ent->um.um_root, mountpoint) != 0) {
            if (ent->um.um_root[0] == '\0') {
                evt_type = UM_ADD;
            } else if (mountpoint[0] == '\0') {
                evt_type = UM_REMOVE;
            } else {
                g_assert_not_reached();
            }
            usb_media_linux_set_um_root(ent, mountpoint);
            if (evt_type == UM_ADD) {
                usb_media_check_syncup((struct usb_media *) ent);
            }
            pthread_rwlock_rdlock(&usb_media_mon.um_rwlock);
            usb_media_monitor_call_evt_hdlr(&usb_media_mon, evt_type,
                    (const struct usb_media *)ent);
            pthread_rwlock_unlock(&usb_media_mon.um_rwlock);
        }
    }
}

struct usb_media_monitor_cleanup_arg {
    struct udev *udev;
    int fd;
};

static void
usb_media_monitor_cleanup(void *arg)
{
    struct usb_media_monitor_cleanup_arg *cln_arg =
            (struct usb_media_monitor_cleanup_arg *) arg;

    close(cln_arg->fd);
    udev_unref(cln_arg->udev);
}

static void *
usb_media_monitor_thread(void *noarg)
{
    struct udev *udev;
    struct udev_monitor *mon;
    struct pollfd fds[2];
    struct usb_media_monitor_cleanup_arg cln_arg;

    udev = udev_new();
    if (!udev) {
        g_critical("udev_new() failed");
        return NULL;
    }

    mon = udev_monitor_new_from_netlink(udev, "udev");
    udev_monitor_filter_add_match_subsystem_devtype(mon, "block", "partition");
    udev_monitor_enable_receiving(mon);
    fds[0].fd = udev_monitor_get_fd(mon);
    fds[0].events = POLLIN;

    /*
     * Start monitoring before enumerating usb media so that no miss between
     * the two action
     */
    usb_media_monitor_query_um(&usb_media_mon, udev);

    /*
     * poll() on /proc/mounts to detect mount change. POLLERR will be returned
     * when mount point changes. See man(5) proc.
     */
    fds[1].fd = open("/proc/mounts", O_RDONLY);
    fds[1].events = POLLERR;

    cln_arg.udev = udev;
    cln_arg.fd = fds[1].fd;
    pthread_cleanup_push(usb_media_monitor_cleanup, &cln_arg);

    while (1) {
        fds[0].revents = 0;
        fds[1].revents = 0;

        if (poll(fds, 2, -1) > 0) {
            int old_cancel_state;
            /* disable cancellation in this section so that all locks are
             * unlocked normally
             */
            pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old_cancel_state);
            if (fds[0].revents & POLLIN) {
                handle_udev_monitor_event(mon);
            }
            if (fds[1].revents & POLLERR) {
                handle_mount_change_event();
            }
            pthread_setcancelstate(old_cancel_state, NULL);
        }
        pthread_testcancel();
    }

    pthread_cleanup_pop(1);
    return NULL;
}



/**
 * @name usb_media.h public functions
 */
/*@{*/

void
usb_media_start_monitor()
{
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    if (pthread_create(&usb_media_mon.tid, &attr, usb_media_monitor_thread,
            NULL)) {
        g_critical("ERROR: failed to create usb monitor thread");
        return;
    }
}

void
usb_media_stop_monitor()
{
    pthread_cancel(usb_media_mon.tid);
}

void
usb_media_register_event_handler(enum um_event_type evt_type,
        um_event_handler_t func)
{
    usb_media_monitor_add_evt_hdlr(&usb_media_mon, evt_type, func);
}

void
usb_media_unregister_event_handler(enum um_event_type evt_type,
        um_event_handler_t func)
{
    usb_media_monitor_del_evt_hdlr(&usb_media_mon, evt_type, func);
}

void
usb_media_check_syncup(struct usb_media *um)
{
    GString *path = g_string_new(um->um_root);
    g_string_append(path, syncup->local_home);
    //g_string_append(path, G_DIR_SEPARATOR_S SYNCUP_HOME_DIR);
    g_debug("remote_home: %s", path->str);
    if (g_access(path->str, F_OK) == 0) {
        um->syncup_enabled = true;
    } else {
        um->syncup_enabled = false;
    }
    g_string_free(path, TRUE);
}

void
usb_media_print_list()
{
    struct list_head *head = &usb_media_mon.um_list;
    struct usb_media_linux *ent;
    pthread_rwlock_rdlock(&usb_media_mon.um_rwlock);
    list_for_each_entry(ent, head, um.um_list) {
        printf("(root path: %s, syncup enabled: %d, dev node: %s)\n",
                ent->um.um_root, ent->um.syncup_enabled, ent->dev_node);
    }
    pthread_rwlock_unlock(&usb_media_mon.um_rwlock);
}
/*@} End of usb_media.h public functions */
